<?php

namespace App\Kuafu\Pay\Channel\WxPay;

use App\Kuafu\Pay\BasePay;
use App\Kuafu\Pay\Channel\WxPay\ApiV2\WxPayApiV2;
use App\Kuafu\Pay\Enum\AliPayEnum;
use App\Kuafu\Pay\Enum\BasePayEnum;
use App\Kuafu\Pay\Enum\PayCodeEnum;
use App\Kuafu\Pay\Enum\WxPayEnum;
use App\Kuafu\Pay\Exceptions\PayException;
use App\Kuafu\Pay\Model\PayStreamModel;
use App\Kuafu\Pay\Service\PayConfigService;
use App\Kuafu\Shop\Service\ShopAppletService;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class WxPay extends BasePay
{
    private WxPayApiV2 $apiV2;
    private            $apiV3;

    /**
     * @var string 直连商户id
     */
    private $subMerchantId;

    /**
     * @var string 支付成功通知地址
     */
    private $payNotifyUrl;

    // 初始化配置信息
    protected function initConfig(int $shopId, int $sysType)
    {
        // apiV2的配置加载，在类内部，单独一份
        $this->apiV2 = new WxPayApiV2($this->payConfig);

        // apiV3的配置加载
        $this->apiV3 = $this->apiV3Load($sysType, $this->payConfig['company'] ?? 0);

        $this->subMerchantId = $this->v3MerchantId;
        $this->payNotifyUrl  = config('pay.wx.notify_url.pay');
    }

    /**
     * 当面付 - 被扫支付
     * @param string $subject
     * @param int $amount
     * @param string $authCode
     * @return array
     * @throws PayException
     */
    public function faceToFacePay(string $subject, int $amount, string $authCode): array
    {
        $outTradeNo = $this->getOutTradeNo();

        $res       = $this->apiV2->microPay($subject, $outTradeNo, $amount, $authCode);
        $formatRes = $this->commonReturn($res);

        $successTime = 0;
        $thirdPayNo  = '';

        if ($formatRes['status']) {

            // 付款成功
            $tradeStatus = BasePayEnum::TRADE_STATUS_OK;
            $successTime = strtotime($formatRes['oriData']['time_end'] ?? '');
            $thirdPayNo  = $formatRes['oriData']['transaction_id'] ?? '';
        } else {
            if ($formatRes['code'] == WxPayEnum::TRADE_STATUS_PAYING) {

                // 等待输入密码
                $tradeStatus = BasePayEnum::TRADE_STATUS_WAIT;
            } else {

                // 失败
                $tradeStatus = BasePayEnum::TRADE_STATUS_CLOSED;
            }
        }

        $formatRes['data'] = [
            'payNo'          => $outTradeNo,
            'thirdPayNo'     => $thirdPayNo,
            'tradeStatus'    => $tradeStatus,
            'paySuccessTime' => $successTime,
        ];

        return $formatRes;
    }

    /**
     * 关闭订单
     * 支付交易返回失败或支付系统超时，调用该接口撤销交易。如果此订单用户支付失败，微信支付系统会将此订单关闭；如果用户支付成功，微信支付系统会将此订单资金退还给用户。
     * @param string $outTradeNo
     * @return array
     */
    public function closePay(string $outTradeNo): array
    {
        try {
            $p = 'out-trade-no';
            $resp = $this->apiV3->v3->pay->transactions->{$p}->{$outTradeNo}->close->post([
                'json' => [
                    'mchid'        => $this->v3MerchantId,
                ]
            ]);

            $resStatusCode = (int)$resp->getStatusCode();
            if ($resStatusCode >= 200 && $resStatusCode < 300) {
                return self::returnFormatData(true, 0, '', []);
            }

            // 理论上不会走到这里
            return self::returnFormatData(false, PayCodeEnum::ERR_APP_PREPAY_ERROR, '关闭支付错误', []);

        } catch (\Exception $e) {

            // 业务逻辑错误，接口返回异常
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r    = $e->getResponse();
                $body = json_decode($r->getBody(), true);
                return self::returnFormatData(false, $body['code'] ?? '', $body['message'] ?? '', []);
            }

            // 其他级别错误
            throw new PayException(PayCodeEnum::ERR_APP_PREPAY_ERROR, $e->getMessage(), $e);
        }
    }

    // 全额退款
    public function refund(string $outTradeNo, int $refundAmount)
    {
        $outRefundNo = $this->getOutTradeNo();

        // 查库，原支付总金额
        $originPayAmount = PayStreamModel::getDataByPayNo($outTradeNo)->payPrice ?? 0;

        $res       = $this->apiV2->refund($outTradeNo, $outRefundNo, $originPayAmount, $refundAmount);
        $formatRes = $this->commonReturn($res);
        if (!$formatRes['status']) {
            return $formatRes;
        }

        $formatRes['data'] = [
            'fundChange'    => true, // 本次退款是否发生了资金变化
            'payNo'         => $outTradeNo,
            'refundNo'      => $outRefundNo, // 退款唯一流水号
            'thirdRefundNo' => $formatRes['oriData']['transaction_id'] ?? '',
            'refundAmount'  => $refundAmount,
        ];

        return $formatRes;
    }

    // 查询订单
    public function query(string $outTradeNo)
    {
        $res       = $this->apiV2->orderQuery($outTradeNo);
        $formatRes = $this->commonReturn($res);
        if (!$formatRes['status']) {
            return $formatRes;
        }

        // 转换支付状态
        $formatRes['data'] = [
            'commonTradeStatus' => $this->convertTradeStatus($formatRes['oriData']['trade_state']),
            'totalPrice'        => $formatRes['oriData']['total_fee'] ?? 0,
            'payNo'             => $outTradeNo,
            'thirdPayNo'        => $formatRes['oriData']['transaction_id'] ?? '',
            'paySuccessTime'    => $formatRes['oriData']['time_end'] ?? 0,
        ];
        return $formatRes;
    }

    // 退款订单查询
    public function refundQuery(string $outTradeNo, string $outRequestNo)
    {
        $res       = $this->apiV2->refundQuery($outRequestNo);
        $formatRes = $this->commonReturn($res);

        $formatRes['data'] = [
            'payNo'             => $outTradeNo,
            'refundNo'          => $outRequestNo,
            'thirdRefundNo'     => $formatRes['oriData']['transaction_id'] ?? '',
            'totalAmount'       => $formatRes['oriData']['total_fee'] ?? 0,
            'refundStatus'      => $this->convertRefundStatus($formatRes['oriData']['refund_status_0'] ?? ''),
            // TODO 这个_0需要确认有没有坑
            'refundSuccessTime' => strtotime($formatRes['oriData']['refund_success_time_0'] ?? ''),
        ];

        return $formatRes;
    }


    /**
     * 对账单
     * @param string $date Y-m-d格式，仅支持3个月内的账单申请
     * @return mixed|void
     */
    public function downloadBill(string $date)
    {
        try {
            $resp = $this->apiV3->v3->bill->fundflowbill->get([
                'query' => [
                    'bill_date' => $date,
                    'account_type' => 'BASIC', // BASIC：基本账户 OPERATION：运营账户 FEES：手续费账户
                    'tar_type' => 'GZIP',
                ],
            ]);

            if ($resp->getStatusCode() == 200) {
                $body              = json_decode($resp->getBody(), true);
                $formatRes         = self::returnFormatData(true, '', '', $body);
                $formatRes['data'] = [
                    'url' => $formatRes['oriData']['download_url'],
                ];
            }
        } catch (\Exception $e) {
            return self::returnFormatData(false, $e->getCode(), $e->getMessage(), []);
        }

        return $formatRes ?? [];
    }

    public function getDownloadFile(string $url, string $date): string
    {
        $query = parse_url($url, PHP_URL_QUERY);
        parse_str($query, $params);

        try {
            $this->apiV3->chain('v3/billdownload/file')->get([
                'query' => ['token' => $params['token']],
//                'debug' => true,
            ]);

        } catch (\Exception $e) {

            // TODO 其他异常情况的处理
            if ($e instanceof RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
//                echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
//                echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
                return $r->getBody();
            }
        }

        return '';
    }

    // 小程序支付
    public function miniPay(string $subject, int $amount, string $openId)
    {
        try {
            $outTradeNo = $this->getOutTradeNo();

            $appId = ShopAppletService::getAppIdByHeaderShopId();
            $resp = $this->apiV3
                ->v3->pay->transactions->jsapi
                ->post([
                    'json' => [
                        'mchid'        => $this->subMerchantId,
                        'out_trade_no' => $outTradeNo,
                        'appid'        => $appId,
                        'description'  => $subject,
                        'notify_url'   => $this->payNotifyUrl,
                        'amount'       => [
                            'total'    => $amount,
                            'currency' => 'CNY'
                        ],
                        'payer'        => [
                            'openid' => $openId,
                        ],
                    ]
                ]);

            if ($resp->getStatusCode() == 200) {
                $body = json_decode($resp->getBody(), true);

                $formatRes = self::returnFormatData(true, '', '', $body);
                $params    = [
                    'appId'     => $appId,
                    'timeStamp' => (string)Formatter::timestamp(),
                    'nonceStr'  => Formatter::nonce(),
                    'package'   => "prepay_id=" . $body['prepay_id'],
                ];

                $params            += [
                    'paySign'  => Rsa::sign(
                        Formatter::joinedByLineFeed(...array_values($params)),
                        $this->merchantPrivateKeyInstance
                    ),
                    'signType' => 'RSA',
                ];
                $formatRes['data'] = [
                    'wx'    => $params,
                    'payNo' => $outTradeNo,
                ];

                return $formatRes;
            }

            // 理论上不会走到这里
            return self::returnFormatData(false, PayCodeEnum::ERR_MINI_PREPAY_ERROR, '预生成支付码错误', []);

        } catch (\Exception $e) {

            // 业务逻辑错误，接口返回异常
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r    = $e->getResponse();
                $body = json_decode($r->getBody(), true);
                return self::returnFormatData(false, $body['code'] ?? '', $body['message'] ?? '', []);
            }

            // 其他级别错误
            throw new PayException(PayCodeEnum::ERR_MINI_PREPAY_ERROR, $e->getMessage(), $e);
        }
    }

    /**
     * 解密APP回调
     * @param array $body
     */
    public function decryptAppCallback(array $body)
    {
        // 不搞那些花里胡哨的验证，直接解密，然后主动查支付记录
        $inBody   = json_encode($body);

        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array)json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        [
            'resource' => [
                'ciphertext'      => $ciphertext,
                'nonce'           => $nonce,
                'associated_data' => $aad
            ]
        ] = $inBodyArray;

        try {
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $this->v3ApiKey, $nonce, $aad);
        } catch (\Exception $e) {
            return self::returnFormatData(false, $e->getCode(), $e->getMessage(), []);
        }

        // 把解密后的文本转换为PHP Array数组
        $inBodyResourceArray = (array)json_decode($inBodyResource, true);

        return $this->queryApiv3($inBodyResourceArray['transaction_id']);
    }

    /**
     * apiV3查询订单
     * @param string $outTradeNo
     * @return array
     * @throws PayException
     */
    public function queryApiv3(string $outTradeNo)
    {
        try {
            $resp = $this->apiV3->v3->pay->transactions->id->$outTradeNo->get([
                'query' => [
                    'mchid' => $this->v3MerchantId,
                ],
            ]);

            if ($resp->getStatusCode() == 200) {
                $body              = json_decode($resp->getBody(), true);
                $formatRes         = self::returnFormatData(true, '', '', $body);
                $formatRes['data'] = [
                    'commonTradeStatus' => $this->convertTradeStatus($body['trade_state']),
                    'totalPrice'        => $body['amount']['total'],
                    'payNo'             => $body['out_trade_no'],
                    'thirdPayNo'        => $body['transaction_id'] ?? '',
                    'paySuccessTime'    => strtotime($body['success_time']),
                ];

                return $formatRes;
            }

            // 理论上不会走到这里
            return self::returnFormatData(false, PayCodeEnum::ERR_PAY_DATA_NOT_EXIST, '查询支付失败', []);
        } catch (\Exception $e) {

            // 业务逻辑错误，接口返回异常
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r    = $e->getResponse();
                $body = json_decode($r->getBody(), true);
                return self::returnFormatData(false, $body['code'] ?? '', $body['message'] ?? '', []);
            }

            // 其他级别错误
            throw new PayException(PayCodeEnum::ERR_PAY_DATA_NOT_EXIST, $e->getMessage(), $e);
        }
    }

    /**
     * 微信APP支付
     * @param string $subject
     * @param int $amount
     * @return array
     * @throws PayException
     */
    public function appPay(string $subject, int $amount, int $expireTime = 0)
    {
        $outTradeNo = $this->getOutTradeNo();

        $params = [
            'mchid'        => $this->v3MerchantId,
            'out_trade_no' => $outTradeNo,
            'appid'        => $this->v3AppId,
            'description'  => $subject,
            'notify_url'   => $this->v3NotifyUrl,
            'amount'       => [
                'total'    => $amount,
                'currency' => 'CNY'
            ],
        ];

        // 超时时间
        if ($expireTime) {
            $expireAt = date('c', $expireTime);
            $params['time_expire'] = $expireAt;
        }

        try {
            $resp = $this->apiV3->v3->pay->transactions->app->post([
                'json' => $params
            ]);

            if ($resp->getStatusCode() == 200) {
                $body = json_decode($resp->getBody(), true);

                $formatRes = self::returnFormatData(true, '', '', $body);

                $appPayData = [
                    'appid'     => $this->v3AppId,
                    'timestamp' => time(),
                    'noncestr'  => $this->apiV2->getNonceStr(),
                    'prepayid'  => $formatRes['oriData']['prepay_id'],
                ];

                $appPayData['sign']      = Rsa::sign(Formatter::joinedByLineFeed(...
                    array_values($appPayData)), $this->merchantPrivateKeyInstance);
                $appPayData['partnerid'] = $this->v3MerchantId;
                $appPayData['package']   = 'Sign=WXPay';

                $formatRes['data'] = [
                    'appPayData' => $appPayData,
                    'payNo'      => $outTradeNo,
                ];

                return $formatRes;
            }

            // 理论上不会走到这里
            return self::returnFormatData(false, PayCodeEnum::ERR_APP_PREPAY_ERROR, '预生成支付码错误', []);
        } catch (\Exception $e) {

            // 业务逻辑错误，接口返回异常
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r    = $e->getResponse();
                $body = json_decode($r->getBody(), true);
                return self::returnFormatData(false, $body['code'] ?? '', $body['message'] ?? '', []);
            }

            // 其他级别错误
            throw new PayException(PayCodeEnum::ERR_APP_PREPAY_ERROR, $e->getMessage(), $e);
        }
    }

    /**
     * H5支付
     * @param string $subject
     * @param int $amount
     * @param string $quitUrl
     * @param string $returnUrl 回跳地址必须在微信后台配置的域名下
     * @return array
     */
    public function wapPay(string $subject, int $amount, string $quitUrl, string $returnUrl)
    {
        $outTradeNo = $this->getOutTradeNo();
        Log::info("wapPay payNo:{$outTradeNo} requestIp: ".request()->ip());

        $resp       = $this->apiV3->v3->pay->transactions->h5->post([
            'json' => [
                'mchid'        => $this->v3MerchantId,
                'out_trade_no' => $outTradeNo,
                'appid'        => $this->v3AppId,
                'description'  => $subject,
                'notify_url'   => $this->v3NotifyUrl,
                'amount'       => [
                    'total'    => $amount,
                    'currency' => 'CNY'
                ],
                "scene_info"   => [
                    "payer_client_ip" => $_SERVER['HTTP_X_REAL_IP'] ?? '127.0.0.1',
                    "h5_info"         => [
                        "type" => "Wap"
                    ]
                ]
            ],
        ]);

        $body = json_decode($resp->getBody(), true);
        if (isset($body['h5_url'])) {
            $body['h5_url'] .= "&redirect_url=".urlencode($returnUrl);
        }

        $formatRes = self::returnFormatData(true, '', '', $body);
        $formatRes['data']['wapPayData'] = $body;
        $formatRes['data']['payNo'] = $outTradeNo;

        return $formatRes;
    }

    public function nativePay(string $subject, int $amount, int $expireTime)
    {
        $outTradeNo = $this->getOutTradeNo();
        Log::info("nativePay payNo:{$outTradeNo} requestIp: ".request()->ip());

        $resp       = $this->apiV3->v3->pay->transactions->native->post([
            'json' => [
                'mchid'        => $this->v3MerchantId,
                'out_trade_no' => $outTradeNo,
                'appid'        => $this->v3AppId,
                'description'  => $subject,
                'notify_url'   => $this->v3NotifyUrl,
                'amount'       => [
                    'total'    => $amount,
                    'currency' => 'CNY'
                ],
                'time_expire' => date('c', $expireTime),
            ],
        ]);

        $body = json_decode($resp->getBody(), true);

        $formatRes = self::returnFormatData(true, '', '', $body);
        $formatRes['data']['wapPayData'] = $body;
        $formatRes['data']['payNo'] = $outTradeNo;

        return $formatRes;
    }

    protected function commonReturn($res): array
    {
        Log::info('WxPay Api Response:', $res);

        $status = true;
        $code   = self::API_RETURN_SUCCESS;
        $msg    = '';

        if (($res['return_code'] ?? '') != self::API_RETURN_SUCCESS) {

            // 接口验签级别错误
            $status = false;
            $code   = $res['return_code'] ?? '';
            $msg    = $res['return_msg'] ?? '';
            return self::returnFormatData($status, $code, $msg, $res);
        }

        if (($res['result_code'] ?? '') != self::API_RETURN_SUCCESS) {

            // 接口逻辑级别错误
            $status = false;
            $code   = $res['err_code'] ?? '';
            $msg    = $res['err_code_des'] ?? '';
            return self::returnFormatData($status, $code, $msg, $res);
        }

        return self::returnFormatData($status, $code, $msg, $res);
    }

    /**
     * 转换支付状态
     * @param string $apiTradeStatus
     * @return int
     */
    protected function convertTradeStatus(string $apiTradeStatus): int
    {
        return [
                WxPayEnum::TRADE_STATUS_WAIT   => BasePayEnum::TRADE_STATUS_WAIT,
                WxPayEnum::TRADE_STATUS_WAIT2  => BasePayEnum::TRADE_STATUS_WAIT,
                WxPayEnum::TRADE_STATUS_PAYING => BasePayEnum::TRADE_STATUS_WAIT,
                WxPayEnum::TRADE_STATUS_CLOSED => BasePayEnum::TRADE_STATUS_CLOSED,
                WxPayEnum::TRADE_STATUS_FAIL   => BasePayEnum::TRADE_STATUS_CLOSED,
                WxPayEnum::TRADE_STATUS_REFUND => BasePayEnum::TRADE_STATUS_CLOSED,
                WxPayEnum::TRADE_STATUS_OK     => BasePayEnum::TRADE_STATUS_OK,
            ][$apiTradeStatus] ?? BasePayEnum::TRADE_STATUS_WAIT;
    }

    /**
     * 转换退款状态
     * @param string $apiRefundStatus
     * @return int
     */
    protected function convertRefundStatus(string $apiRefundStatus): int
    {
        return [
                WxPayEnum::REFUND_STATUS_OK    => BasePayEnum::REFUND_STATUS_OK,
                WxPayEnum::REFUND_STATUS_CLOSE => BasePayEnum::REFUND_STATUS_ERROR,
                WxPayEnum::REFUND_STATUS_ING   => BasePayEnum::REFUND_STATUS_WAIT,
                WxPayEnum::REFUND_STATUS_ERROR => BasePayEnum::REFUND_STATUS_ERROR,
            ][$apiRefundStatus] ?? BasePayEnum::REFUND_STATUS_WAIT;
    }

    private $merchantPrivateKeyInstance;
    private $merchantCertificateSerial;

    private string $v3MerchantId;
    private string $v3AppId;
    private string $v3NotifyUrl;
    private string $v3MerchantPrivateKeyFilePath;
    private string $v3MerchantCertificateFilePath;
    private string $v3PlatformCertificateFilePath;
    private string $v3ApiKey;

    /**
     * apiV3类接口，配置加载
     * @return \WeChatPay\BuilderChainable
     */
    private function apiV3Load(int $sysType, int $company)
    {
        // 商户号，假定为`1000100`
        $this->v3MerchantId = $this->payConfig['mch_id'];
        $this->v3AppId      = $this->payConfig['app_id'];
        $this->v3ApiKey     = $this->payConfig['api_key'];
        $this->v3NotifyUrl  = config('other.GATEWAY_URL') . '/api/pay/callback/wx/appPayV2/' . $sysType . '/' . $company;

        // 商户私钥，文件路径假定为 `/path/to/merchant/apiclient_key.pem`
        $this->v3MerchantPrivateKeyFilePath = $this->payConfig['ssl_key_path'];
        // 加载商户私钥
        $this->merchantPrivateKeyInstance = PemUtil::loadPrivateKey($this->v3MerchantPrivateKeyFilePath);

        $this->v3MerchantCertificateFilePath = $this->payConfig['ssl_cert_path'];
        // 加载商户证书
        $merchantCertificateInstance = PemUtil::loadCertificate($this->v3MerchantCertificateFilePath);
        // 解析商户证书序列号
        $this->merchantCertificateSerial = PemUtil::parseCertificateSerialNo($merchantCertificateInstance);

        $this->v3PlatformCertificateFilePath = $this->payConfig['wx_cert_path'];
        // 加载平台证书
        $platformCertificateInstance = PemUtil::loadCertificate($this->v3PlatformCertificateFilePath);
        // 解析平台证书序列号
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateInstance);

        // 工厂方法构造一个实例
        return Builder::factory([
            'mchid'      => $this->v3MerchantId,
            'serial'     => $this->merchantCertificateSerial,
            'privateKey' => $this->merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformCertificateInstance,
            ],
        ]);
    }

    /**
     * 获取手续费
     * @param int $payPrice
     * @return float
     */
    public function getFeeAmtByPayPrice(int $payPrice): array
    {
        return [
            round($payPrice * WxPayEnum::PAYMENT_RATE), // fee
            number_format(WxPayEnum::PAYMENT_RATE, 6, '.', ''), // feeRate
        ];
    }
}
